<?php
namespace Plu\Library\Services\Redis;

use Redis as RedisClient;
use RedisCluster;
use Plu\Library\Services\ServiceBase;

class RedisService {
    private $_name;

    private $_configProvider;

    private $_redisMaster = null;

    private $_redisSlave = null;

    private $_configs = null;

    private $_lastMethod = null;

    private $_lastArguments = null;

    private $_yac = null;

    private $_validMethods = [
        /*
            #KEY AND STRING
            **append - Append a value to a key
            bitCount - Count set bits in a string
            **bitOp - Perform bitwise operations between strings
            **decr, decrBy - Decrement the value of a key
            get - Get the value of a key
            getBit - Returns the bit value at offset in the string value stored at key
            getRange - Get a substring of the string stored at a key
            **getSet - Set the string value of a key and return its old value
            **incr, incrBy - Increment the value of a key
            **incrByFloat - Increment the float value of a key by the given amount
            mGet, getMultiple - Get the values of all the given keys
            **mSet, mSetNX - Set multiple keys to multiple values
            **set - Set the string value of a key
            **setBit - Sets or clears the bit at offset in the string value stored at key
            **setEx, pSetEx - Set the value and expiration of a key
            **setNx - Set the value of a key, only if the key does not exist
            **setRange - Overwrite part of a string at key starting at the specified offset
            strLen - Get the length of the value stored in a key
         */
        'bitcount',
        'get',
        'getbit',
        'getrange',
        'mget', 
        'getmultiple',
        'strlen',
        'scan',
        /*
            #HASH
            **hDel - Delete one or more hash fields
            hExists - Determine if a hash field exists
            hGet - Get the value of a hash field
            hGetAll - Get all the fields and values in a hash
            **hIncrBy - Increment the integer value of a hash field by the given number
            **hIncrByFloat - Increment the float value of a hash field by the given amount
            hKeys - Get all the fields in a hash
            hLen - Get the number of fields in a hash
            hMGet - Get the values of all the given hash fields
            **hMSet - Set multiple hash fields to multiple values
            **hSet - Set the string value of a hash field
            **hSetNx - Set the value of a hash field, only if the field does not exist
            hVals - Get all the values in a hash
            hScan - Scan a hash key for members
            hStrLen - Get the string length of the value associated with field in the hash
         */
        'hexists',
        'hget',
        'hgetall',
        'hkeys',
        'hlen',
        'hmget',
        'hvals',
        'hscan',
        'hstrlen',
        /*
            #LIST
            **blPop, brPop - Remove and get the first/last element in a list
            **bRPopLPush - Pop a value from a list, push it to another list and return it
            lIndex, lGet - Get an element from a list by its index
            **lInsert - Insert an element before or after another element in a list
            lLen, lSize - Get the length/size of a list
            **lPop - Remove and get the first element in a list
            **lPush - Prepend one or multiple values to a list
            **lPushx - Prepend a value to a list, only if the list exists
            lRange, lGetRange - Get a range of elements from a list
            **lRem, lRemove - Remove elements from a list
            **lSet - Set the value of an element in a list by its index
            **lTrim, listTrim - Trim a list to the specified range
            **rPop - Remove and get the last element in a list
            **rPopLPush - Remove the last element in a list, append it to another list and return it (redis >= 1.1)
            **rPush - Append one or multiple values to a list
            **rPushX - Append a value to a list, only if the list exists
         */
        'lindex',
        'lget',
        'llen', 
        'lsize',
        'lrange', 
        'lgetrange',
        /*
            #Sets
            **sAdd - Add one or more members to a set
            sCard, sSize - Get the number of members in a set
            sDiff - Subtract multiple sets
            **sDiffStore - Subtract multiple sets and store the resulting set in a key
            sInter - Intersect multiple sets
            **sInterStore - Intersect multiple sets and store the resulting set in a key
            sIsMember, sContains - Determine if a given value is a member of a set
            sMembers, sGetMembers - Get all the members in a set
            **sMove - Move a member from one set to another
            **sPop - Remove and return one or more members of a set at random
            sRandMember - Get one or multiple random members from a set
            **sRem, sRemove - Remove one or more members from a set
            **sUnion - Add multiple sets
            **sUnionStore - Add multiple sets and store the resulting set in a key
            sScan - Scan a set for members
         */
        'scard',
        'ssize',
        'sdiff',
        'sinter',
        'sismember',
        'scontains',
        'smembers',
        'srandmember',
        'sscan',
        /*
            #Sorted sets
            **zAdd - Add one or more members to a sorted set or update its score if it already exists
            zCard, zSize - Get the number of members in a sorted set
            zCount - Count the members in a sorted set with scores within the given values
            **zIncrBy - Increment the score of a member in a sorted set
            **zInter - Intersect multiple sorted sets and store the resulting sorted set in a new key
            zRange - Return a range of members in a sorted set, by index
            zRangeByScore, zRevRangeByScore - Return a range of members in a sorted set, by score
            zRangeByLex - Return a lexigraphical range from members that share the same score
            zRank, zRevRank - Determine the index of a member in a sorted set
            **zRem, zDelete - Remove one or more members from a sorted set
            **zRemRangeByRank, zDeleteRangeByRank - Remove all members in a sorted set within the given indexes
            **zRemRangeByScore, zDeleteRangeByScore - Remove all members in a sorted set within the given scores
            zRevRange - Return a range of members in a sorted set, by index, with scores ordered from high to low
            zScore - Get the score associated with the given member in a sorted set
            **zUnion - Add multiple sorted sets and store the resulting sorted set in a new key
            zScan - Scan a sorted set for members
         */
        'zcard',
        'zsize',
        'zcount',
        'zrange',
        'zrangebyscore',
        'zrangebylex',
        'zrank',
        'zrevrank',
        'zrevrange',
        'zscore',
        'zscan',
    ];

    const REDIS_HMGET_MAX_LEN = 100;

    /**
     * 
     */
    public function __construct($name) {
        $this->_name = $name;
        $this->_configProvider = new ServiceBase;

        $this->_configs = $this->_getConfig($name);


        //兼容没有cluster的配置
        if (strpos($this->_name, "cluster") > 0) {
            if (!isset($this->_configs['cluster'])) {
                $this->_configs['cluster'] = true;
                $this->_configs['master'] = [ $this->_getConfig($name) ];
            }
        }

        $this->_yac = new \Yac("rd_");
    }

    private function _getConfig($name) {
        $_config = $this->_configProvider->config->redis->get($name);
        if ($_config) {
            return $_config->toArray();
        }

        return false;
    }

    private function _connectMaster() {
        //lazy load
        if($this->_redisMaster !== null && $this->_redisMaster != false){
            return;
        }

        //cluster is a main pool.
        if (isset($this->_configs['cluster']) && ($this->_configs['cluster'] === true)) {
            return $this->_connectCluster();
        }
        $this->_redisMaster = false;
        //TODO 兼容老配置文件
        if (isset($this->_configs['hostname'])) {
            $this->_redisMaster = $this->_connectRedis($this->_configs);
        } elseif (isset($this->_configs['master'])) {
            $count = count($this->_configs['master']);
            $config = $this->_configs['master'][rand(0, $count - 1)];
            $this->_redisMaster = $this->_connectRedis($config);
        }
    }

    private function _connectSlave() {
        //lazy load
        if($this->_redisSlave !== null && $this->_redisSlave != false){
            return;
        }

        //cluster is a main pool.
        if (isset($this->_configs['cluster']) && ($this->_configs['cluster'] === true)) {
            return $this->_connectCluster();
        }
        $this->_redisSlave = false;

        if (isset($this->_configs['hostname'])) {
            //try get slave config
            $_config = $this->_getConfig(sprintf("%s.slave", $this->_name));
            if ($_config === false) {
                $_config = $this->_configs;
            }
            $this->_redisSlave = $this->_connectRedis($_config);
        } elseif (isset($this->_configs['slave'])) {
            $count = count($this->_configs['slave']);
            $config = $this->_configs['slave'][rand(0, $count - 1)];
            $this->_redisSlave = $this->_connectRedis($config);
        }
    }

    private function _connectCluster() {
        $this->_redisSlave = $this->_redisMaster = false;
        $configs = [];
        if(isset($this->_configs['slave'])){
            $configs = $this->_configs['slave'];
        }
        if(isset($this->_configs['master'])){
            $configs = $this->_configs['master'];
        }

        if(empty($configs)){
            return;
        }
        $this->_redisSlave = $this->_redisMaster = $this->_connectRedisCluster($configs);;
    }

    private function _connectRedis($config) {
        //try redis connect
        $is_successed = false;
        try {
            $timeout = isset($config['timeout']) ? $config['timeout'] : 5;
            $redis = new RedisClient();

            if ($config['pconnect']) {
                $is_successed = $redis->pconnect($config['hostname'], $config['port'], $timeout);
            } else {
                $is_successed = $redis->connect($config['hostname'], $config['port'], $timeout);
            }

            if ($is_successed && !empty($config['password'])) {
                $is_successed = $redis->auth($config['password']);
            }

            if (isset($config['server'])) {
                $redis->select($config['server']);
            } else {
                $redis->select(0);
            }

        } catch (\RedisException $e) {
            throw $e;
            return false;
        }

        return $redis;
    }

    private function _connectRedisCluster($configs) {
        if (count($configs) === 0) {
            return false;
        }

        $timeout = isset($configs[0]['timeout'])? $configs[0]['timeout'] : 5;
        $pconnect = isset($configs[0]['pconnect'])? $configs[0]['pconnect'] : 1;
        $password = isset($configs[0]['password'])? $configs[0]['password'] : false;

        //try redis connect
        $is_successed = false;
        try {

            $clusterConfig = [];
            foreach($configs as $config){
                $clusterConfig[] = $config['hostname'].':'.$config['port'];
            }

            $cluster = new RedisCluster( NULL, $clusterConfig, $timeout, $timeout, $pconnect);
            $cluster->setOption(RedisCluster::OPT_SLAVE_FAILOVER, RedisCluster::FAILOVER_DISTRIBUTE);

        } catch (\RedisException $e) {
            throw $e;
            return false;
        } catch (\RedisClusterException $e) {
            throw $e;
            return false;
        }

        return $cluster;
    }

    public function __call($method, $arguments) {
        $isMaster = true;
        if (in_array(strtolower($method), $this->_validMethods)) {
            $isMaster = false;
        }

        $redis = $this->getRedis($isMaster);

        //master fail
        if ($redis === false || $redis === null) {
            throw new \Exception('Redis server could not be connected');
        }

        if ($this->_lastMethod == 'select') {
            call_user_func_array(array($redis, $this->_lastMethod), $this->_lastArguments);
        }

        $this->_lastMethod = strtolower($method);
        $this->_lastArguments = $arguments;
        return call_user_func_array(array($redis, $method), $arguments);
    }

    public function getRedis($isMaster = false) {
        if (!$isMaster) {
            $this->_connectSlave();
            $redis = $this->_redisSlave;
        }

        if ($isMaster || $redis === false || $redis === null) {
            $this->_connectMaster();
            $redis = $this->_redisMaster;
        }

        return $redis;
    }

    private function _yacGet($key) {
        if (is_array($key)) {
            $items = $this->_yac->get($key);
            foreach ($items as $key => $value) {
                $items[$key] = json_decode($value, true);
            }

            return $items;
        } else {
            $item = $this->_yac->get($key);
            if ($item === false) {
                return null;
            } else {
                return json_decode($item, true);
            }
        }
    }

    private function _yacSet() {
        $numArgs = func_num_args();
        $items = [];

        if ($numArgs == 2) { // 2 keys ttl
            list($items, $ttl) = func_get_args();
        } elseif ($numArgs == 3) { // 3 key value ttl
            list($key, $value, $ttl) = func_get_args();
            $items[$key] = $value;
        }

        //将value json化
        foreach ($items as $key => $value) {
            $items[$key] = json_encode($value);
        }

        $this->_yac->set($items, $ttl);
    }

    /**
     * 优化版本hMGet读取
     */
    public function getFieldsFromHash($key, $fields = []) {
        $temp = [];

        for ($c = 0, $len = count($fields); $c < $len; $c = $c + self::REDIS_HMGET_MAX_LEN) {
            $items = $this->hMGet($key, array_slice($fields, $c, self::REDIS_HMGET_MAX_LEN));

            if (!empty($items) && is_array($items)) {
                if (empty($temp)) {
                    $temp = $items;
                } else {
                    $temp = array_merge($temp, $items);
                }
            }
        }

        return $temp;
    }

    /**
     * 优化mGet
     */
    public function getFields($keys) {
        $temp = [];

        for ($c = 0, $len = count($keys); $c < $len; $c = $c + self::REDIS_HMGET_MAX_LEN) {
            $_keys = array_slice($keys, $c, self::REDIS_HMGET_MAX_LEN);

            $items = $this->mGet($_keys);

            if (!empty($items) && is_array($items)) {
                if (empty($temp)) {
                    $temp = $items;
                } else {
                    $temp = array_merge($temp, $items);
                }
            }
        }

        return $temp;
    }

    /**
     * get后的结果缓存在本地
     */
    public function getFieldsWithCache($keys, $ttl = 60) {
        $temp = [];
        $storeItems = [];

        for ($c = 0, $len = count($keys); $c < $len; $c++) {
            $key = $keys[$c];
        
            $item = $this->_yacGet($key);

            if (is_null($item)) {
                $item = $this->get($key);
                $storeItems[$key] = $item;
            }

            $temp[] = $item;
        }

        if (!empty($storeItems)) {
            $this->_yacSet($storeItems, rand($ttl, $ttl + 10) );
        }

        return $temp;
    }

    public function getFieldWithCache($key, $ttl = 60) {
        $item = $this->_yacGet($key);

        if (is_null($item)) {
            $item = $this->get($key);
            $this->_yacSet($key, $item, rand($ttl, $ttl + 10) );
        }

        return $item;
    }

    public function getFieldFromHashWithCache($key, $field, $ttl = 60) {
        $item = $this->_yacGet($key.$field);
        if (is_null($item)) {
            $item = $this->hGet($key, $field);
            $this->_yacSet($key.$field, $item, rand($ttl, $ttl + 10) );
        }
        return $item;
    }
}
